﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Morus.Presentation
{
    public sealed class PresenterBinder
    {
        private static IPresenterFactory _factory;
        private static IPresenterDiscoveryStrategy _discoveryStrategy;
        private static readonly ICompositeViewTypeFactory _compositeViewTypeFactory = new DefaultCompositeViewTypeFactory();
        private readonly IMessageCoordinator _messageCoordinator = new MessageCoordinator();
        private readonly IEnumerable<object> _hosts;
        private readonly IList<IView> _viewInstancesRequiringBinding = new List<IView>();
        private readonly IList<IPresenter> _presenters = new List<IPresenter>();
        private bool _initialBindingHasBeenPerformed;

        public PresenterBinder(object host)
            : this(new[] { host }) { }

        public PresenterBinder(IEnumerable<object> hosts)
        {
            _hosts = hosts.ToList();

            foreach (var selfHostedView in hosts.OfType<IView>())
            {
                RegisterView(selfHostedView);
            }
        }

        public static IPresenterFactory Factory
        {
            [DebuggerStepThrough]
            get
            {
                return _factory ?? (_factory = new DefaultPresenterFactory());
            }
            set
            {
                Check.Argument.IsNotNull(value, "value");

                if (_factory != null)
                {
                    throw new InvalidOperationException(
                        _factory is DefaultPresenterFactory
                        ? "The factory has already been set, and can be not changed at a later time. In this case, it has been set to the default implementation. This happens if the factory is used before being explicitly set. If you wanted to supply your own factory, you need to do this in your Application_Start event."
                        : "You can only set your factory once, and should really do this in Application_Start.");
                }
                _factory = value;
            }
        }
        public static IPresenterDiscoveryStrategy DiscoveryStrategy
        {
            [DebuggerStepThrough]
            get
            {
                return _discoveryStrategy ?? (_discoveryStrategy = new CompositePresenterDiscoveryStrategy());
            }
            set
            {
                Check.Argument.IsNotNull(value, "value");

                _discoveryStrategy = value;
            }
        }

        public IMessageCoordinator MessageCoordinator
        {
            [DebuggerStepThrough]
            get
            {
                return _messageCoordinator;
            }
        }

        public void RegisterView(IView viewInstance)
        {
            Check.Argument.IsNotNull(viewInstance, "viewInstance");

            // Registering view instance of type viewInstance.GetType().FullName));
            _viewInstancesRequiringBinding.Add(viewInstance);

            // If an initial binding has already been performed, go ahead
            // and bind this view straight away. This allows us to bind
            // dynamically created controls that are added after Page.Init.
            if (_initialBindingHasBeenPerformed)
            {
                PerformBinding();
            }
        }

        public void PerformBinding()
        {
            try
            {
                if (_viewInstancesRequiringBinding.Any())
                {
                    var newPresenters = PerformBinding(_hosts, _viewInstancesRequiringBinding.Distinct(), DiscoveryStrategy, _messageCoordinator, Factory);

                    _presenters.AddRange(newPresenters);

                    _viewInstancesRequiringBinding.Clear();
                }
            }
            finally
            {
                _initialBindingHasBeenPerformed = true;
            }
        }

        public void Release()
        {
            MessageCoordinator.Close();

            lock (_presenters)
            {
                foreach (var presenter in _presenters)
                {
                    _factory.Release(presenter);
                }
                _presenters.Clear();
            }
        }

        private static IEnumerable<IPresenter> PerformBinding(IEnumerable<object> hosts, IEnumerable<IView> candidates, IPresenterDiscoveryStrategy presenterDiscoveryStrategy, IMessageBus messageBus, IPresenterFactory presenterFactory)
        {
            var bindings = GetBindings(hosts, candidates, presenterDiscoveryStrategy);

            var newPresenters = BuildPresenters(messageBus, presenterFactory, bindings);

            return newPresenters;
        }

        private static IEnumerable<PresenterBinding> GetBindings(IEnumerable<object> hosts, IEnumerable<IView> candidates, IPresenterDiscoveryStrategy presenterDiscoveryStrategy)
        {
            var results = presenterDiscoveryStrategy.GetBindings(hosts, candidates);

            ThrowExceptionsForViewsWithNoPresenterBound(results);

            return results.SelectMany(r => r.Bindings);
        }

        private static void ThrowExceptionsForViewsWithNoPresenterBound(IEnumerable<PresenterDiscoveryResult> results)
        {
            var resultToThrowExceptionsFor = results
                .Where(r => r.Bindings.IsEmpty())
                .Where(r => r
                    .ViewInstances
                    .Where(v => v.ThrowExceptionIfNoPresenterBound)
                    .Any())
                .FirstOrDefault();

            if (resultToThrowExceptionsFor == null)
            {
                return;
            }

            throw new InvalidOperationException(@"Failed to find presenter for view instance of {0}.

{1}

If you do not want this exception to be thrown, set ThrowExceptionIfNoPresenterBound to false on your view.".FormatWith(
                resultToThrowExceptionsFor
                    .ViewInstances
                    .Where(v => v.ThrowExceptionIfNoPresenterBound)
                    .First()
                    .GetType()
                    .FullName,
                resultToThrowExceptionsFor.Message
            ));
        }

        private static IEnumerable<IPresenter> BuildPresenters(IMessageBus messageBus, IPresenterFactory presenterFactory, IEnumerable<PresenterBinding> bindings)
        {
            return bindings
                .SelectMany(binding =>
                    BuildPresenters(messageBus, presenterFactory, binding));
        }

        private static IEnumerable<IPresenter> BuildPresenters(IMessageBus messageBus, IPresenterFactory presenterFactory, PresenterBinding binding)
        {
            IEnumerable<IView> viewsToCreateFor;

            switch (binding.BindingMode)
            {
                case BindingMode.Default:
                    viewsToCreateFor = binding.ViewInstances;
                    break;
                case BindingMode.SharedPresenter:
                    viewsToCreateFor = new[]
                    {
                        CreateCompositeView(binding.ViewType, binding.ViewInstances)
                    };
                    break;
                default:
                    throw new NotSupportedException("Binding mode {0} is not supported by this method.".FormatWith(binding.BindingMode));
            }

            return viewsToCreateFor.Select(viewInstance =>
                BuildPresenter(messageBus, presenterFactory, binding, viewInstance));
        }

        private static IPresenter BuildPresenter(IMessageBus messageBus, IPresenterFactory presenterFactory, PresenterBinding binding, IView viewInstance)
        {
            var presenter = presenterFactory.Create(binding.PresenterType, binding.ViewType, viewInstance);
            presenter.Messages = messageBus;

            return presenter;
        }

        internal static IView CreateCompositeView(Type viewType, IEnumerable<IView> childViews)
        {
            var compositeViewType = _compositeViewTypeFactory.BuildCompositeViewType(viewType);
            var view = (ICompositeView)Activator.CreateInstance(compositeViewType);
            foreach (var v in childViews)
            {
                view.Add(v);
            }
            return view;
        }
    }
}
